/***************************************************************************
 *   Copyright (C) 2011 by Alexander S. Salieff                            *
 *   salieff@mail.ru                                                       *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include <stdio.h>
#include <stdlib.h>

#include <vector>
#include <algorithm>

#include <QDBusMessage>
#include <QDBusInterface>
#include <QDBusArgument>
#include <QDesktopWidget>
#include <QMessageBox>
#include <QSizePolicy>
#include <QStyle>
#include <QStyleOption>
#include <QStyleHintReturnMask>
#include <QStyleOptionMenuItem>
#include <QStyleOptionFrame>

#include <QPainter>
#include <QDebug>

#include "yaudtrayapp.h"
#include "devinfowidget.h"
#include "helpers.h"

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
NoMediaWidget::NoMediaWidget(QWidget *parent)
    : QWidget(parent),
      iconLabel(NULL),
      textLabel(NULL)
{
    hbLayout = new QHBoxLayout(this);

    QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);

    iconLabel = new QLabel(this);
    iconLabel->setSizePolicy(sizePolicy);
    iconLabel->setMinimumSize(QSize(48, 48));
    iconLabel->setMaximumSize(QSize(48, 48));
    iconLabel->setPixmap(yaudIcon("media-playback-stop").pixmap(48, 48));

    hbLayout->addWidget(iconLabel);

    textLabel = new QLabel(tr("No media available"), this);
    QFont font;
    font.setBold(true);
    font.setItalic(true);
    font.setWeight(75);
    textLabel->setFont(font);

    hbLayout->addWidget(textLabel);
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
NoMediaWidget::~NoMediaWidget()
{
    delete iconLabel;
    delete textLabel;
    delete hbLayout;
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
MountWidget::MountWidget(QWidget *parent)
    : QWidget(parent, Qt::Popup)
{
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
MountWidget::~MountWidget()
{
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
bool MountWidget::event(QEvent *e)
{
    switch (e->type())
    {
    case QEvent::Resize :
    {
        QStyleHintReturnMask menuMask;
        QStyleOption option;
        option.initFrom(this);
        if (style()->styleHint(QStyle::SH_Menu_Mask, &option, this, &menuMask))
        {
            setMask(menuMask.region);
        }
        break;
    }

    default:
        break;
    }
    return QWidget::event(e);
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void MountWidget::paintEvent(QPaintEvent *)
{
    QPainter p(this);
    QRegion emptyArea = QRegion(rect());

    QStyleOptionMenuItem menuOpt;
    menuOpt.initFrom(this);
    menuOpt.state = QStyle::State_None;
    menuOpt.checkType = QStyleOptionMenuItem::NotCheckable;
    menuOpt.maxIconWidth = 0;
    menuOpt.tabWidth = 0;
#if QT_VERSION >= 0x040500
    style()->drawPrimitive(QStyle::PE_PanelMenu, &menuOpt, &p, this);
#endif
    const int fw = style()->pixelMetric(QStyle::PM_MenuPanelWidth, 0, this);

    //draw border
    if (fw)
    {
        QRegion borderReg;
        borderReg += QRect(0, 0, fw, height()); //left
        borderReg += QRect(width()-fw, 0, fw, height()); //right
        borderReg += QRect(0, 0, width(), fw); //top
        borderReg += QRect(0, height()-fw, width(), fw); //bottom
        p.setClipRegion(borderReg);
        emptyArea -= borderReg;
        QStyleOptionFrame frame;
        frame.rect = rect();
        frame.palette = palette();
        frame.state = QStyle::State_None;
        frame.lineWidth = style()->pixelMetric(QStyle::PM_MenuPanelWidth);
        frame.midLineWidth = 0;
        style()->drawPrimitive(QStyle::PE_FrameMenu, &frame, &p, this);
    }

    //finally the rest of the space
    p.setClipRegion(emptyArea);
    menuOpt.state = QStyle::State_None;
    menuOpt.menuItemType = QStyleOptionMenuItem::EmptyArea;
    menuOpt.checkType = QStyleOptionMenuItem::NotCheckable;
    menuOpt.rect = rect();
    menuOpt.menuRect = rect();
    style()->drawControl(QStyle::CE_MenuEmptyArea, &menuOpt, &p, this);
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
YaudTrayApp::YaudTrayApp(int &argc, char **argv)
    : QApplication(argc, argv),
      trayIcon(NULL),
      mountWidget(NULL),
      mountLayout(NULL),
      aboutMenu(NULL),
      aboutAction(NULL),
      exitAction(NULL),
      noMediaWidget(NULL)
{
    createTrayIcon();

    if (!connectToUdisks2())
        exit(1);

    if (!getDevicesList())
        exit(1);
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
YaudTrayApp::~YaudTrayApp()
{
    std::map<QString, YaudDeviceInfo*>::iterator it;
    for (it = devices.begin(); it != devices.end(); ++it)
    {
        mountLayout->removeWidget(it->second->menuWidget);
        delete it->second->menuWidget;
        delete it->second;
    }
    devices.clear();

    mountLayout->removeWidget(noMediaWidget);
    delete noMediaWidget;
    delete mountLayout;
    delete mountWidget;

    delete aboutAction;
    delete exitAction;
    delete aboutMenu;

    delete trayIcon;
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::createTrayIcon()
{
#if QT_VERSION >= 0x040600
    printf("themeName: %s\n", qPrintable(QIcon::themeName()));
    QStringList tsPaths = QIcon::themeSearchPaths();
    for (int i=0; i<tsPaths.size(); ++i)
        printf("themeSearchPaths[%d]: %s\n", i, qPrintable(tsPaths.at(i)));
#endif

    setWindowIcon(yaudIcon("media-eject"));
    setQuitOnLastWindowClosed(false);

    mountWidget = new MountWidget();
    mountLayout = new QVBoxLayout(mountWidget);
    noMediaWidget = new NoMediaWidget(mountWidget);
    noMediaWidget->show();
    mountLayout->addWidget(noMediaWidget);

    aboutMenu = new QMenu(tr("Yaud tray"));
    aboutAction = aboutMenu->addAction(yaudIcon("help-about"), tr("&About"), this, SLOT(onAbout()));
    exitAction = aboutMenu->addAction(yaudIcon("application-exit"), tr("&Quit"), this, SLOT(quit()));

    trayIcon = new QSystemTrayIcon(yaudIcon("media-eject"));
    trayIcon->setContextMenu(aboutMenu);

    connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(toggleTrayMenu(QSystemTrayIcon::ActivationReason)));

    trayIcon->show();
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
bool YaudTrayApp::connectToUdisks2()
{
    if (!QDBusConnection::systemBus().isConnected())
    {
        fprintf(stderr, "ERROR: Can't connect to dbus daemon\n");
        return false;
    }

    if (!QDBusConnection::systemBus().connect("org.freedesktop.UDisks2",
            "/org/freedesktop/UDisks2",
            "org.freedesktop.UDisks2",
            "DeviceAdded",
            this,
            SLOT(onDeviceAdded(QDBusObjectPath))))
    {
        fprintf(stderr, "ERROR: Can't connect to org.freedesktop.UDisks2/DeviceAdded\n");
        return false;
    }


    if (!QDBusConnection::systemBus().connect("org.freedesktop.UDisks2",
            "/org/freedesktop/UDisks2",
            "org.freedesktop.UDisks2",
            "DeviceRemoved",
            this,
            SLOT(onDeviceRemoved(QDBusObjectPath))))
    {
        fprintf(stderr, "ERROR: Can't connect to org.freedesktop.UDisks2/DeviceRemoved\n");
        return false;
    }

    if (!QDBusConnection::systemBus().connect("org.freedesktop.UDisks2",
            "/org/freedesktop/UDisks2",
            "org.freedesktop.UDisks2",
            "DeviceChanged",
            this,
            SLOT(onDeviceChanged(QDBusObjectPath))))
    {
        fprintf(stderr, "ERROR: Can't connect to org.freedesktop.UDisks2/DeviceChanged\n");
        return false;
    }

    return true;
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
static bool sortByUdisks2Path(const QDBusObjectPath &p1, const QDBusObjectPath &p2)
{
    return p1.path() < p2.path();
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
bool YaudTrayApp::getDevicesList()
{
    QDBusInterface devEnum("org.freedesktop.UDisks2",
                           "/org/freedesktop/UDisks2",
                           "org.freedesktop.UDisks2",
                           QDBusConnection::systemBus());

    QDBusMessage enumRes = devEnum.call("EnumerateDevices");
    if (enumRes.type() == QDBusMessage::ErrorMessage)
    {
        fprintf(stderr, "ERROR: Can't call EnumerateDevices\n");
        fprintf(stderr, "       %s : %s\n", qPrintable(enumRes.errorName()), qPrintable(enumRes.errorMessage()));
        return false;
    }

    if (enumRes.type() != QDBusMessage::ReplyMessage || !enumRes.arguments().at(0).canConvert<QDBusArgument>())
    {
        fprintf(stderr, "ERROR: Unexpected result type of EnumerateDevices call\n");
        return false;
    }

    const QDBusArgument enumArg = enumRes.arguments().at(0).value<QDBusArgument>();
#if QT_VERSION >= 0x040500
    if (enumArg.currentType() != QDBusArgument::ArrayType)
    {
        fprintf(stderr, "ERROR: Unexpected argument type of EnumerateDevices call\n");
        return false;
    }
#endif

    std::vector<QDBusObjectPath> sortVector;

    enumArg.beginArray();
    while (!enumArg.atEnd())
    {
        QDBusObjectPath devObj = qdbus_cast<QDBusObjectPath>(enumArg);
        sortVector.push_back(devObj);
    }
    enumArg.endArray();

    std::sort(sortVector.begin(), sortVector.end(), sortByUdisks2Path);

    for (size_t i=0; i<sortVector.size(); ++i)
        addDevice(sortVector[i], true);

    return true;
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::addDevice(QDBusObjectPath device, bool onStart)
{
    YaudDeviceInfo *yaudDI = new YaudDeviceInfo(device);
    if (!yaudDI->isExternalMountPoint)
    {
        delete yaudDI;
        return;
    }

    printf("[YaudTrayApp::onDeviceAdded]\n");
    yaudDI->print();

    DevInfoWidget *widg = new DevInfoWidget(yaudDI, mountWidget);
    connect(widg, SIGNAL(requestProcessing(QString)), this, SLOT(processingRequested(QString)));
    connect(widg, SIGNAL(requestCloseError(QString)), this, SLOT(errCloseRequested(QString)));

    widg->show();

    yaudDI->menuWidget = widg;
    devices[yaudDI->udisks2Path] = yaudDI;

    connect(yaudDI, SIGNAL(commandDone(QString)), this, SLOT(deviceCommandDone(QString)));
    connect(yaudDI, SIGNAL(commandError(QString)), this, SLOT(deviceCommandError(QString)));

    noMediaWidget->hide();
    mountLayout->insertWidget(0, widg);

    if (!onStart)
        showTrayMenu();
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::onDeviceAdded(QDBusObjectPath device)
{
    addDevice(device);
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::onDeviceRemoved(QDBusObjectPath device)
{
    std::map<QString, YaudDeviceInfo*>::iterator it = devices.find(device.path());
    if (it == devices.end())
        return;

    printf("[YaudTrayApp::onDeviceRemoved] : %s\n", qPrintable(device.path()));

    mountLayout->removeWidget(it->second->menuWidget);
    delete it->second->menuWidget;
    delete it->second;
    devices.erase(it);

    if (devices.empty())
        noMediaWidget->show();

    if (!mountWidget->isHidden())
        showTrayMenu();
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::onDeviceChanged(QDBusObjectPath device)
{
    std::map<QString, YaudDeviceInfo*>::iterator it = devices.find(device.path());
    if (it == devices.end())
    {
        onDeviceAdded(device);
        return;
    }

    it->second->convert(device);
    if (!it->second->isExternalMountPoint)
    {
        onDeviceRemoved(device);
        return;
    }

    printf("[YaudTrayApp::onDeviceChanged]\n");
    it->second->print();

    it->second->refreshWidget();

    if (!mountWidget->isHidden())
        showTrayMenu();
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::onAbout()
{
    QMessageBox::about(NULL,
                       tr("YAUDTray"),
                       tr("<H3 ALIGN=CENTER>Yet Another Udisks2 Tray Mounter</H3>\n"
                          "<A HREF=\"https://code.google.com/p/yaudtray2\" ALIGN=CENTER>https://code.google.com/p/yaudtray2</A>"));
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::showTrayMenu()
{
    mountWidget->adjustSize();
    QPoint p(trayIcon->geometry().topLeft().x() - (mountWidget->width()/2), trayIcon->geometry().topLeft().y() - mountWidget->height());

    // Am I screen outbound?
    QRect scrRect = QApplication::desktop()->screenGeometry();
    if (p.x() < scrRect.x()) p.setX(scrRect.x()); // Outbound left
    if ( (p.x()+mountWidget->width()) > (scrRect.x()+scrRect.width()) ) p.setX(scrRect.x()+scrRect.width()-mountWidget->width()); // Outbound right
    if (p.y() < scrRect.y()) p.setY(scrRect.y()); // Outbound top
    if ( (p.y()+mountWidget->height()) > (scrRect.y()+scrRect.height()) ) p.setY(scrRect.y()+scrRect.height()-mountWidget->height()); // Outbound bottom

    mountWidget->move(p);
    mountWidget->show();
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::toggleTrayMenu(QSystemTrayIcon::ActivationReason reason)
{
    if (reason == QSystemTrayIcon::Context)
        return;

    if (mountWidget->isHidden())
        showTrayMenu();
    else
        mountWidget->hide();
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::processingRequested(QString udisks2Path)
{
    std::map<QString, YaudDeviceInfo*>::iterator it = devices.find(udisks2Path);
    if (it == devices.end())
        return;

    if (it->second->isMounted)
    {
        if (!it->second->unmount())
            fprintf(stderr, "ERROR: Can't send umount command to dbus!\n");
    }
    else
    {
        if (!it->second->mount())
            fprintf(stderr, "ERROR: Can't send mount command to dbus!\n");
    }
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::deviceCommandDone(QString udisks2Path)
{
    std::map<QString, YaudDeviceInfo*>::iterator it = devices.find(udisks2Path);
    if (it == devices.end())
        return;

    // Eject CD after unmount
    if (it->second->isEjectable && !it->second->isMounted)
    {
        if (!it->second->eject())
            fprintf(stderr, "ERROR: Can't send eject command to dbus!\n");
    }

    if (!mountWidget->isHidden())
        showTrayMenu();
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::deviceCommandError(QString)
{
    showTrayMenu();
}

// --------========++++++++ooooooooOOOOOOOOoooooooo++++++++========--------
void YaudTrayApp::errCloseRequested(QString udisks2Path)
{
    std::map<QString, YaudDeviceInfo*>::iterator it = devices.find(udisks2Path);
    if (it == devices.end())
        return;

    it->second->lastError.clear();
    it->second->lastErrDescription.clear();

    it->second->refreshWidget();
    if (!mountWidget->isHidden())
        showTrayMenu();
}
